package com.google.code.UtopiaXml;

/**
 * Class that generates code for parser of XML document with elements
 * corresponding to given classes
 *
 * @author Michael Simacek, Roman Lacko
 */
public class ParserGenerator {

    private Schema schema;
    private String parserName;
    private boolean validate = true;
    private final String exceptionName = "RuntimeException";

    /**
     * Creates new ParserGenerator with default name "Parser"
     *
     * @param schema file name of the schema that will be used for validation
     */
    public ParserGenerator(Schema schema) {
        this(schema, "Parser");
    }

    /**
     * Creates new ParserGenrator with given name
     *
     * @param schema file name of the schema that will be used for validation
     * @param parserName name of the requested parser class
     */
    public ParserGenerator(Schema schema, String parserName) {
        this.schema = schema;
        this.parserName = CodeBuilder.toUpperCamelCase(parserName);
    }

    /**
     * Generates parser class
     *
     * @param packageName pagkage name of the generated class or empty String
     * for default
     * @return ClassBuilder containig code of the generated class
     */
    public ClassBuilder generateParserClass(String packageName) {
        ClassBuilder clb = new ClassBuilder(parserName, packageName);
        CodeBuilder cb = clb.getClassContentBuilder();

        cb.appendLine(String.format("private static final String SCHEMA = \"%s\";", schema.getSchemaName()));
        cb.newLine();

        cb.appendLine("private Node getNextElement(Node node) {");
        cb.addImport("org.w3c.dom.Node");
        cb.appendLine("do {");
        cb.appendLine("node = node.getNextSibling();");
        cb.appendLine("} while(node != null && node.getNodeType() != Node.ELEMENT_NODE);");
        cb.appendLine("return node;");
        cb.appendLine("}");
        cb.newLine();

        cb.appendLine("private Node getFirstElement(Node node) {");
        cb.appendLine("node = node.getFirstChild();");
        cb.appendLine("while(node != null && node.getNodeType() != Node.ELEMENT_NODE) {");
        cb.appendLine("node = node.getNextSibling();");
        cb.appendLine("}");
        cb.appendLine("return node;");
        cb.appendLine("}");
        cb.newLine();

        generateRootParserMethod(cb);

        for (ClassTemplate ct : schema.getClasses()) {
            ct.generateParseMethod(cb);
        }

        return clb;
    }

    /**
     * Generates method for parsing input document into root class
     *
     * @param cb CodeBuilder to append code to
     */
    private void generateRootParserMethod(CodeBuilder cb) {
        cb.addImport("java.io.File");
        cb.addImport("java.io.IOException");
        cb.addImport("org.w3c.dom.Document");
        cb.addImport("org.xml.sax.SAXException");
        cb.addImport("org.w3c.dom.Node");
        cb.addImport("javax.xml.parsers.DocumentBuilder");
        cb.addImport("javax.xml.parsers.DocumentBuilderFactory");
        cb.addImport("javax.xml.parsers.FactoryConfigurationError");
        cb.addImport("javax.xml.parsers.ParserConfigurationException");
        if (validate) {
            cb.addImport("javax.xml.validation.Schema");
            cb.addImport("javax.xml.validation.SchemaFactory");
            cb.addImport("javax.xml.validation.Validator");
            cb.addImport("javax.xml.transform.stream.StreamSource");
            cb.addImport("javax.xml.XMLConstants");
        }

        cb.appendLine("public java.lang.Object parse(File file) throws IOException {");

        cb.appendLine("if (file == null) {");
        cb.appendLine("throw new IllegalArgumentException(\"file is null\");");
        cb.appendLine("}");

        // XSD validation
        if (validate) {
            cb.appendLine("try {");
            cb.appendLine("SchemaFactory sfact     = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);");
            cb.appendLine("URL schemaFile = getClass().getResource(SCHEMA);");
            cb.addImport("java.net.URL");
            cb.appendLine("if (schemaFile == null) {");
            cb.appendLine("System.err.println(\"Schema \" + SCHEMA + \" not found, skipping validation.\");");
            cb.appendLine("} else {");
            cb.appendLine("Schema        schema    = sfact.newSchema(schemaFile);");
            cb.appendLine("Validator     validator = schema.newValidator();");
            cb.appendLine("validator.validate(new StreamSource(file));");
            cb.appendLine("}");
            cb.appendLine("} catch(SAXException ex) {");
            cb.appendLine("throw new IllegalArgumentException(\"The specified file is not valid\");");
            cb.appendLine("}");
            cb.newLine();
        }

        // Document building
        cb.appendLine("try {");
        cb.appendLine("DocumentBuilderFactory docbf = DocumentBuilderFactory.newInstance();");
        cb.appendLine("DocumentBuilder        docb  = docbf.newDocumentBuilder();");
        cb.appendLine("Document               doc   = docb.parse(file);");
        cb.newLine();
        cb.appendLine("String nodeName  = doc.getDocumentElement().getNodeName();");
        cb.newLine();

        // Invoke parser
        for (ComplexType root : schema.getRoots()) {
            String className = root.getOriginalName();
            cb.appendLine(String.format("if (nodeName.equals(\"%s\")) {", className));
            cb.appendLine(String.format("return parse%s(doc.getDocumentElement());", root.getName()));
            cb.appendLine("} else");
        }

        // if the classname is unknown (well, it shouldn't be, but let's be sure)
        cb.appendLine("{");
        cb.appendLine("throw new IllegalArgumentException(String.format(\"Invalid node name '%s'\", nodeName));");
        cb.appendLine("}");

        cb.appendLine("} catch (ParserConfigurationException ex) {");
        cb.appendLine("throw new " + exceptionName + "(\"Document builder error\", ex);");
        cb.appendLine("} catch (FactoryConfigurationError ex) {");
        cb.appendLine("throw new " + exceptionName + "(\"Document factory error\", ex);");
        cb.appendLine("} catch (SAXException ex) {");
        cb.appendLine("throw new " + exceptionName + "(\"Document parsing error\", ex);");
        cb.appendLine("}");

        cb.appendLine("}");

        //parsing date
        cb.addImport("java.text.DateFormat");
        cb.addImport("java.text.SimpleDateFormat");
        cb.addImport("java.util.Date");
        cb.newLine();
        cb.appendLine("private Date parseDate(String date) {");
        cb.appendLine("try {");
        cb.appendLine("DateFormat dateFormatter = new SimpleDateFormat(\"yyyy-MM-dd\");");
        cb.appendLine("return dateFormatter.parse(date.replace(\"\\n\", \"\"));");
        cb.appendLine("} catch (ParseException ex) {");
        cb.addImport("java.text.ParseException");
        cb.appendLine("throw new " + exceptionName + "(\"date parsing errror\", ex);");
        cb.appendLine("}");
        cb.appendLine("}");
        cb.newLine();
        cb.appendLine("private Date parseTime(String time) {");
        cb.appendLine("try {");
        cb.appendLine("DateFormat dateFormatter = new SimpleDateFormat(\"HH:mm:ss\");");
        cb.appendLine("return dateFormatter.parse(time.replace(\"\\n\", \"\"));");
        cb.appendLine("} catch (ParseException ex) {");
        cb.addImport("java.text.ParseException");
        cb.appendLine("throw new " + exceptionName + "(\"time parsing errror\", ex);");
        cb.appendLine("}");
        cb.appendLine("}");
        cb.newLine();
        cb.appendLine("private Date parseDateTime(String datetime) {");
        cb.appendLine("try {");
        cb.appendLine("DateFormat dateFormatter = new SimpleDateFormat(\"yyyy-MM-dd'T'HH:mm:ss\");");
        cb.appendLine("return dateFormatter.parse(datetime.replace(\"\\n\", \"\"));");
        cb.appendLine("} catch (ParseException ex) {");
        cb.addImport("java.text.ParseException");
        cb.appendLine("throw new " + exceptionName + "(\"datetime parsing errror\", ex);");
        cb.appendLine("}");
        cb.appendLine("}");

        cb.newLine();
    }
}
